Skip to content

Stop forwarding caller-supplied bearers, and stop advertising the wikis' issuers - #518

Merged
alistair3149 merged 1 commit into
masterfrom
deprecate-bearer-passthrough
Jul 30, 2026
Merged

Stop forwarding caller-supplied bearers, and stop advertising the wikis' issuers#518
alistair3149 merged 1 commit into
masterfrom
deprecate-bearer-passthrough

Conversation

@alistair3149

@alistair3149 alistair3149 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Closes #504.

With the hosted OAuth proxy disabled, an Authorization header was forwarded to MediaWiki verbatim, and /.well-known/oauth-protected-resource named the wikis' own authorization servers. Together those steered a client into minting a token at the wiki and presenting it here — the shape MCP forbids, since a server must not accept tokens that were not issued for it.

What changes

Forwarding requires MCP_ALLOW_BEARER_PASSTHROUGH=true, is deprecated, and warns at startup. Without it a request carrying a bearer is refused 401 rather than served: ignoring the header would run the request anonymously or as a configured identity while the caller believed it was acting as itself.

Nothing advertises the wikis' authorization servers any more. With the proxy off, /.well-known/oauth-protected-resource answers 404, and list-wikis omits each wiki's authorizationServer — it named where to mint a token this server now refuses. The 401 challenge carries resource_metadata only when that document exists; otherwise it is a bare Bearer challenge, which RFC 6750 admits.

A deployment that can no longer serve anything says so: the server warns at startup when a wiki requires a signed-in user and neither hosted sign-in nor forwarding can provide one. A private default wiki is stranded even with static credentials, because the challenge precedes credential resolution. The tool-layer error names the operator's action rather than asking for a token the transport would refuse.

Two exported signatures tightened, in both cases because omitting the argument now means something dangerous rather than something useful: buildProtectedResource takes a required authorizationServers, and createOAuthProtectedResourceHandler a required getProxyConfig (pass () => null for no proxy).

What a reviewer should weigh

The refusal is deliberately not keyed on oauth2ClientId. That option describes how this server runs browser sign-in, not whether the wiki accepts bearers — any wiki with Extension:OAuth does. Keying on it would keep forwarding tokens on exactly the documented public read-only and manual-token shapes. A test covers that configuration.

The advertisement is removed even under the opt-in. Forwarding is documented for callers that already hold a token, so they need no discovery, and restoring it would reinstate the steering this closes.

Client impact is narrower than the diff

No client-negotiation surface is touched: register.ts, cimd.ts, redirectPolicy.ts and asMetadata.ts are unchanged, so hosted-sign-in deployments behave identically — discovery, dynamic registration and CIMD all as before.

UpstreamAsMetadata carries no registration_endpoint, so MediaWiki offers no dynamic client registration and a client that discovered a wiki's issuer could never self-register. The advertisement was never a zero-install path. The one real break is manual-token setup — the population this deprecation exists to migrate.

Verification

Full suite 1,696 green; typecheck, lint (zero warnings) and format clean.

Confirmed end to end against a real MediaWiki 1.43.6 wiki, with a request tap between this server and the wiki recording the Authorization header of everything we send upstream.

proxy off, no opt-in proxy off, opt-in proxy on
/.well-known/oauth-protected-resource 404 404 200, authorization_servers names only the proxy
POST with a caller bearer 401, nothing reaches the wiki 200, token forwarded 401, nothing reaches the wiki
401 challenge bare Bearer adds resource_metadata
list-wikis authorization server absent advertised absent
auth_shape anonymous bearer-passthrough oauth-proxy

Anonymous reads return real page content and do reach the wiki in every column, so the "nothing reaches the wiki" cells are genuine negatives rather than a probe that stopped working. A private wiki that can be signed into by no route warns at startup and names the setting to change. The refusal was also exercised against a configuration with no oauth2ClientId anywhere, the shape the gate described above would have missed.

Each behaviour change is mutation-bound: reverting the refusal, restoring the advertisement, ungating the tokenless challenge, or making the challenge always advertise each fail a test.

Open

The deprecation says forwarding will be removed without naming a release.

src/runtime/authShape.ts reads process.env through a default parameter rather than the explicit injection evaluateBearerGuard uses. Threading an environment through the per-request call sites would be invasive for a cosmetic gain, so it is tested directly instead.

@alistair3149
alistair3149 force-pushed the deprecate-bearer-passthrough branch from 47baeb2 to 7556077 Compare July 30, 2026 15:00
@alistair3149
alistair3149 marked this pull request as ready for review July 30, 2026 15:08
@alistair3149
alistair3149 force-pushed the deprecate-bearer-passthrough branch from 7556077 to f118dbe Compare July 30, 2026 15:37
…is' issuers

With the hosted proxy disabled, an Authorization header was forwarded to
MediaWiki verbatim and the protected-resource document named the wikis' own
authorization servers. Together those steered a client into minting a token
at the wiki and presenting it here - the shape MCP forbids, since a server
must not accept tokens that were not issued for it.

Forwarding now requires MCP_ALLOW_BEARER_PASSTHROUGH=true, is deprecated,
and warns at startup. Without it a request carrying a bearer is refused with
401 rather than served: ignoring the header would run the request anonymously
or as a configured identity while the caller believed it was acting as
itself, which is worse than refusing it.

The refusal is deliberately unconditional and NOT keyed on whether a wiki
sets oauth2ClientId. That option describes how this server runs browser
sign-in, not whether the wiki accepts bearers - any wiki with Extension:OAuth
does - so keying on it would keep forwarding tokens on exactly the documented
public read-only and manual-token deployments. A test covers that shape.

The advertisement is removed unconditionally, including under the opt-in.
Only the hosted proxy makes this server an authorization server, so with the
proxy off /.well-known/oauth-protected-resource now answers 404. The
documented use for forwarding is callers that already hold a token, so they
need no discovery, and restoring it would reinstate the steering this closes.
Answering before the upstream fetches also means an unauthenticated request
no longer costs one outbound metadata fetch per OAuth wiki.

Because that document can now be absent, the 401 challenge only advertises
resource_metadata when it exists; RFC 6750 admits a challenge without it.
Otherwise every challenge pointed a client at a URL that 404s.

The all-OAuth 401 challenge is gated on the opt-in: it asks a caller to
supply a wiki token, and with no way to forward one the same condition is an
operator misconfiguration instead. So the server now warns at startup when a
wiki needs a signed-in user but neither hosted sign-in nor forwarding can
provide one, and the tool-layer error names the operator action rather than
asking for a token the transport would refuse.

Tests that cover the forwarding shape now run under the flag, since the
behaviour still exists there. The multi-wiki issuer listing is deleted rather
than gated - that behaviour is gone. The fake authorization server gained a
metadata-request counter so the no-fetch claim is actually asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alistair3149
alistair3149 force-pushed the deprecate-bearer-passthrough branch from f118dbe to 56f3942 Compare July 30, 2026 16:07
@alistair3149
alistair3149 merged commit af870f0 into master Jul 30, 2026
1 check passed
@alistair3149
alistair3149 deleted the deprecate-bearer-passthrough branch July 30, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deprecate the per-request bearer token passthrough on the HTTP transport

1 participant